S00-00 General-Cloudflare-D1、R2、KV
[TOC]
D1
D1(边缘关系型数据库):
你想全面了解 Cloudflare D1 数据库,包括它的定位、核心特性、使用流程、与 Workers/Hono 的结合方式,以及实战中的常用操作——接下来我会从「基础认知→核心优势→实操流程→实战示例→最佳实践」层层拆解,重点聚焦 D1 在边缘开发场景的实际应用,确保内容通俗易懂且能直接落地。
什么是 D1
什么是 Cloudflare D1?:
Cloudflare D1 是 Cloudflare 推出的开源、SQLite 兼容的边缘关系型数据库,专为 Cloudflare 全球边缘网络设计,核心定位是解决「边缘应用的结构化数据存储与复杂查询」需求。
核心背景与定位:
- 填补生态空白:Cloudflare 原有存储方案(KV 键值、R2 对象、Durable Objects 状态)均不适合结构化、关联型数据(如用户信息、订单、商品列表),D1 则专门适配这类场景;
- 边缘原生:数据部署在 Cloudflare 全球 300+ 数据中心,就近响应请求,延迟低至毫秒级;
- 低学习成本:完全兼容 SQLite 语法(最流行的轻量 SQL 数据库),无需学习新的 SQL 方言;
- 无缝衔接 Workers/Pages:与 Cloudflare 边缘运行时深度整合,代码中可直接通过
env访问,无额外网络请求开销。
简单来说:D1 是 Cloudflare 边缘应用的「MySQL/PostgreSQL 平替」,轻量、低延迟、适配边缘场景,适合开发需要 SQL 能力的边缘应用(如用户系统、内容管理、订单管理)。
D1 核心优势
D1 核心优势(对比传统数据库/Cloudflare 其他存储):
| 特性维度 | Cloudflare D1 | 传统云数据库(MySQL/PostgreSQL) | Cloudflare KV(键值存储) |
|---|---|---|---|
| 部署位置 | 全球边缘节点 | 中心化集群 | 全球边缘节点 |
| 访问延迟 | 极低(就近响应) | 较高(需跨网络) | 极低 |
| SQL 支持 | 完整 SQLite 语法 | 完整 SQL 语法 | 无(仅键值查询) |
| 关联查询 | 支持(JOIN/子查询) | 支持 | 不支持 |
| 事务 | 支持 ACID 特性 | 支持 | 不支持 |
| 学习成本 | 低(SQLite 语法) | 中(不同方言差异) | 极低 |
| 适用场景 | 边缘结构化数据、复杂查询 | 中心化大规模数据存储 | 简单键值、高频读写 |
D1 核心能力总结:
- SQLite 100% 兼容:支持
SELECT/INSERT/UPDATE/DELETE、JOIN、子查询、索引、事务等核心 SQL 能力; - 边缘低延迟:数据就近存储,全球用户访问延迟比中心化数据库低一个数量级;
- Workers 无缝集成:代码中直接通过
c.env调用,无需配置连接字符串、处理网络超时; - 免费额度友好:开发测试阶段完全免费(1GB 存储、100 万次查询/月),满足小型应用需求;
- 开发体验佳:支持 Wrangler 命令行管理、交互式 SQL 终端、数据导入/导出。
D1 前置准备
D1 前置准备:
使用 D1 前需完成基础环境配置,确保后续操作顺畅:
- Wrangler 版本要求:需安装 v3.0 及以上版本(D1 核心功能在 v3 中完善):bash
# 安装/升级 Wrangler npm install -g wrangler # 验证版本 wrangler -v # 输出 >= 3.0.0 即可 - 登录 Cloudflare 账户:bash
wrangler login # 自动打开浏览器授权 - 基础 SQL 知识:了解 SQLite 基础语法(建表、增删改查),无需深入进阶特性。
D1 基础 Wrangler 命令行操作
D1 的数据库管理主要通过 Wrangler 命令完成,以下是高频使用的命令:
创建 D1 数据库
创建 D1 数据库:
# 交互式创建(推荐,自动生成数据库名称)
wrangler d1 create <DB_NAME> # 如 wrangler d1 create my-first-d1-db
# 非交互式创建(指定区域)
wrangler d1 create <DB_NAME> --location eu # 区域可选:us/eu/apac 等- 执行成功后,终端会输出数据库 ID 和 绑定配置代码(需复制到
wrangler.toml,核心!); - 示例输出:
✨ Successfully created DB 'my-first-d1-db' (ID: 12345678-1234-1234-1234-1234567890ab) ⚡️ Add the following to your wrangler.toml: [[d1_databases]] binding = "MY_DB" # 代码中通过 env.MY_DB 访问 database_name = "my-first-d1-db" database_id = "12345678-1234-1234-1234-1234567890ab"
查看数据库列表
查看数据库列表:
wrangler d1 list- 输出账户下所有 D1 数据库的名称、ID、状态,方便核对数据库是否创建成功。
执行 SQL 文件
执行 SQL 文件(初始化表结构):
# 执行本地 SQL 文件(如 schema.sql)
wrangler d1 execute <DB_NAME> --file=./schema.sql
# 示例:执行建表 SQL
# schema.sql 内容:
# CREATE TABLE IF NOT EXISTS users (
# id INTEGER PRIMARY KEY AUTOINCREMENT,
# name TEXT NOT NULL,
# email TEXT UNIQUE NOT NULL,
# created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
# );
wrangler d1 execute my-first-d1-db --file=./schema.sql交互式 SQL 终端
交互式 SQL 终端(调试/临时操作):
wrangler d1 execute <DB_NAME> --interactive- 进入 SQLite 风格的交互式终端,可直接输入 SQL 语句执行(如
SELECT * FROM users;); - 适合调试表结构、临时插入测试数据、验证查询语句。
执行单条 SQL 语句
执行单条 SQL 语句(快速测试):
# 执行插入语句
wrangler d1 execute my-first-d1-db --command="INSERT INTO users (name, email) VALUES ('Cloudflare', 'test@cf.com')"
# 执行查询语句
wrangler d1 execute my-first-d1-db --command="SELECT * FROM users;"绑定 D1 到 Workers 项目
绑定 D1 到 Workers 项目:
将创建数据库时输出的配置添加到项目的 wrangler.toml 中,才能在代码中访问 D1:
# wrangler.toml 核心配置
name = "d1-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"
# D1 数据库绑定(关键!)
[[d1_databases]]
binding = "MY_DB" # 代码中通过 c.env.MY_DB 访问
database_name = "my-first-d1-db"
database_id = "12345678-1234-1234-1234-1234567890ab" # 替换为你的数据库 ID实战:D1 结合 Hono 开发边缘应用
以下是完整的「Hono + D1」开发流程,实现用户信息的 CRUD 操作,可直接复制使用:
步骤 1:初始化 Hono 项目:
# 初始化项目
wrangler init d1-hono-demo
cd d1-hono-demo
# 安装 Hono
npm install hono步骤 2:配置 wrangler.toml:
按上文要求,添加 D1 绑定配置(替换为你的数据库 ID/名称)。
步骤 3:编写 D1 表结构(schema.sql):
在项目根目录创建 schema.sql:
-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入测试数据
INSERT OR IGNORE INTO users (name, email, age) VALUES
('Alice', 'alice@cf.com', 25),
('Bob', 'bob@cf.com', 30);步骤 4:执行 SQL 初始化表结构:
wrangler d1 execute my-first-d1-db --file=./schema.sql步骤 5:编写 Hono + D1 核心代码(src/index.ts):
import { Hono } from 'hono'
// 创建 Hono 实例
const app = new Hono()
// 1. 查询所有用户
app.get('/users', async (c) => {
// 通过 c.env.MY_DB 访问 D1 数据库
const { results } = await c.env.MY_DB.prepare('SELECT * FROM users ORDER BY created_at DESC').all() // all() 返回所有结果,first() 返回第一条,run() 执行无返回值
return c.json({
code: 200,
data: results
})
})
// 2. 根据 ID 查询单个用户
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
// 使用参数绑定(防止 SQL 注入,核心!)
const { results } = await c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?').bind(id).all()
if (results.length === 0) {
return c.json({ code: 404, msg: 'User not found' }, { status: 404 })
}
return c.json({
code: 200,
data: results[0]
})
})
// 3. 创建新用户(POST 请求)
app.post('/users', async (c) => {
try {
const { name, email, age } = await c.req.json()
// 验证参数
if (!name || !email) {
return c.json({ code: 400, msg: 'Name and email are required' }, { status: 400 })
}
// 插入数据
const result = await c.env.MY_DB.prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)')
.bind(name, email, age)
.run()
return c.json(
{
code: 201,
msg: 'User created',
data: { id: result.meta.last_row_id }
},
{ status: 201 }
)
} catch (err) {
// 捕获唯一键冲突(email 重复)
if (err.message.includes('UNIQUE constraint failed')) {
return c.json({ code: 409, msg: 'Email already exists' }, { status: 409 })
}
return c.json({ code: 500, msg: 'Server error' }, { status: 500 })
}
})
// 4. 更新用户信息
app.put('/users/:id', async (c) => {
const id = c.req.param('id')
const { name, age } = await c.req.json()
const result = await c.env.MY_DB.prepare('UPDATE users SET name = ?, age = ? WHERE id = ?').bind(name, age, id).run()
if (result.meta.changes === 0) {
return c.json({ code: 404, msg: 'User not found' }, { status: 404 })
}
return c.json({ code: 200, msg: 'User updated' })
})
// 5. 删除用户
app.delete('/users/:id', async (c) => {
const id = c.req.param('id')
const result = await c.env.MY_DB.prepare('DELETE FROM users WHERE id = ?').bind(id).run()
if (result.meta.changes === 0) {
return c.json({ code: 404, msg: 'User not found' }, { status: 404 })
}
return c.json({ code: 200, msg: 'User deleted' })
})
// 导出 Workers 处理函数
export default app.fetch步骤 6:本地调试与部署:
# 本地调试(启动开发服务器)
wrangler dev
# 部署到 Cloudflare 线上
wrangler deploy测试接口(示例):
- 访问
http://localhost:8787/users→ 查看所有用户; - POST 请求
http://localhost:8787/users,Body 传{"name":"Charlie","email":"charlie@cf.com","age":28}→ 创建用户; - PUT 请求
http://localhost:8787/users/3,Body 传{"name":"Charlie Updated","age":29}→ 更新用户; - DELETE 请求
http://localhost:8787/users/3→ 删除用户。
D1 进阶技巧与注意事项
防止 SQL 注入
防止 SQL 注入(核心!):
- 必须使用
bind()方法绑定参数,而非直接拼接 SQL 字符串(如SELECT * FROM users WHERE id = ${id}会导致注入); - 示例:
prepare('SELECT * FROM users WHERE id = ?').bind(id)是安全写法。
事务支持
事务支持:
D1 支持 ACID 事务,适合多步操作(如订单创建+库存扣减):
app.post('/transaction-demo', async (c) => {
// 开启事务
const tx = c.env.MY_DB.transaction()
try {
// 执行多个操作
tx.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('Dave', 'dave@cf.com')
tx.prepare('UPDATE users SET age = ? WHERE email = ?').bind(35, 'dave@cf.com')
// 提交事务
await tx.commit()
return c.json({ msg: 'Transaction success' })
} catch (err) {
// 回滚事务
await tx.rollback()
return c.json({ msg: 'Transaction failed', error: err.message }, { status: 500 })
}
})索引优化
索引优化:
对高频查询的字段创建索引,提升查询性能:
-- 为 email 字段创建索引(加速按 email 查询)
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
-- 为 age 字段创建索引(加速按 age 筛选)
CREATE INDEX IF NOT EXISTS idx_users_age ON users (age);配额与限制
配额与限制(免费版):
- 存储上限:1GB;
- 每月查询次数:100 万次;
- 单条 SQL 执行超时:5 秒;
- 不支持异地容灾(需手动备份)。
数据备份与导出
数据备份与导出:
# 导出 D1 数据为 SQL 文件
wrangler d1 execute <DB_NAME> --command=".dump" > backup.sql
# 导入备份数据
wrangler d1 execute <DB_NAME> --file=backup.sqlD1 常用属性与方法
Cloudflare D1 常用属性与方法详细解析:
Cloudflare D1 的所有操作均基于Wrangler 绑定后的 D1 数据库实例(如 Hono 中通过 c.env.MY_DB 访问的实例,以下简称D1 实例),同时执行 SQL 后返回的结果对象包含核心属性(如查询结果、执行元数据),这两类是开发中最常用的核心对象。
本次将以D1 实例的常用方法(SQL 执行、事务、批量操作)+执行结果对象的常用属性(核心返回值)为核心,结合 Cloudflare Workers/Hono 实际开发场景,逐一讲解语法、作用、使用示例,所有内容均为边缘开发高频用法,可直接落地。
核心前提:
- D1 实例需先在
wrangler.toml中配置绑定([[d1_databases]]),才能在代码中通过c.env.<BINDING_NAME>访问; - D1 完全兼容 SQLite 语法,所有方法均围绕SQL 预编译、参数绑定、执行设计,必须使用参数绑定(
bind)防止 SQL 注入,这是 D1 开发的核心规范; - 所有 D1 操作均为异步方法,需配合
async/await使用。
D1 实例的核心常用方法:
D1 实例(如 c.env.MY_DB)是操作数据库的唯一入口,方法分为基础 SQL 执行、事务处理、批量操作三大类,其中 prepare 是所有 SQL 操作的基础(预编译 SQL 语句)。
模块 1:基础 SQL 执行
所有 SQL 操作的统一流程:prepare(SQL语句) → bind(参数) → 执行方法(all/first/get/run),核心是通过预编译+参数绑定实现安全的 SQL 执行。
prepare()
:prepare(sql: string) - SQL 预编译(基础方法)
- 语法:
d1Instance.prepare(sql) - 核心作用:接收原始 SQL 语句(含占位符
?),创建预编译语句对象,后续可通过bind绑定参数、调用执行方法(all/first 等),是 D1 所有 SQL 操作的前置步骤。 - 关键:SQL 语句中用问号
?作为参数占位符,不可直接拼接变量(防止注入)。 - 示例:typescript
// 创建预编译语句对象,? 是参数占位符 const stmt = c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?')
bind()
:bind(...params: any[]) - 参数绑定(配合 prepare 使用)
- 语法:
prepare(sql).bind(p1, p2, ...pn) - 核心作用:为预编译语句的
?占位符按顺序绑定参数,支持任意类型(字符串、数字、布尔、null),绑定后返回可执行的语句对象,可直接调用执行方法。 - 关键:参数数量、类型需与 SQL 中的
?一一对应。 - 示例:typescript
// 单个参数绑定 c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?').bind(1) // 多个参数绑定(按顺序对应 ?) c.env.MY_DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('Alice', 'alice@cf.com')
all()
:all() - 执行查询并返回所有结果(查多条)
语法:
prepare(sql).bind(...params).all()核心作用:执行 SQL 语句(主要用于
SELECT),返回符合条件的所有数据,是查询多条记录的首选方法。返回值:
{results,meta},包含results(数组,所有查询结果)和meta(执行元数据)的对象。Cloudflare+Hono 示例(查询所有用户/多条件查询):
typescript// 查所有用户 app.get('/users', async (c) => { const res = await c.env.MY_DB.prepare('SELECT * FROM users ORDER BY created_at DESC').all() // 无参数可直接调用 all() return c.json({ data: res.results, meta: res.meta }) }) // 多参数查询(按年龄和名称模糊查询) app.get('/users/filter', async (c) => { const age = c.req.query('age') || 0 const name = c.req.query('name') || '' const res = await c.env.MY_DB.prepare('SELECT * FROM users WHERE age > ? AND name LIKE ?') .bind(age, `%${name}%`) .all() return c.json({ count: res.results.length, data: res.results }) })
first()
:first([colName?: string]) - 执行查询并返回第一条结果(查单条)
语法:
prepare(sql).bind(...params).first()/first(colName)核心作用:执行 SQL 语句,仅返回第一条查询结果,适合单条记录查询(如按 ID 查用户);若传入列名,可直接返回该列的单个值。
返回值:
- 无列名:返回第一条结果的对象(无结果则为
undefined); - 有列名:返回第一条结果中该列的单个值(无结果则为
undefined)。
- 无列名:返回第一条结果的对象(无结果则为
Cloudflare+Hono 示例(按 ID 查用户/直接获取单个列值):
typescript// 按 ID 查单个用户(返回完整对象) app.get('/users/:id', async (c) => { const res = await c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?').bind(c.req.param('id')).first() if (!res) return c.json({ msg: 'User not found' }, { status: 404 }) return c.json({ data: res }) }) // 直接获取单个列值(如获取用户名称) app.get('/users/:id/name', async (c) => { const userName = await c.env.MY_DB.prepare('SELECT name FROM users WHERE id = ?') .bind(c.req.param('id')) .first('name') // 直接返回 name 列的值 return c.json({ name: userName || 'Unknown' }) })
get()
:get([colName?: string]) - 执行查询并返回单个值(查单个字段)
语法:
prepare(sql).bind(...params).get()/get(colName)核心作用:专为获取单个值设计(如计数、查单个字段),比
first更轻量,若查询结果有多行/多列,仅返回第一行第一列的值。返回值:单个原始值(字符串/数字/布尔/null),无结果则为
undefined。Cloudflare+Hono 示例(统计用户总数/查单个字段):
typescript// 统计用户总数(核心用法) app.get('/users/count', async (c) => { const total = await c.env.MY_DB.prepare('SELECT COUNT(*) FROM users').get() // 直接返回计数结果(数字) return c.json({ total_users: total }) }) // 查单个字段(等价于 first('email'),更简洁) app.get('/users/:id/email', async (c) => { const email = await c.env.MY_DB.prepare('SELECT email FROM users WHERE id = ?').bind(c.req.param('id')).get('email') return c.json({ email: email || 'Unknown' }) })
run()
:run() - 执行无返回结果的 SQL(增/删/改)
语法:
prepare(sql).bind(...params).run()核心作用:执行非查询类 SQL(
INSERT/UPDATE/DELETE/CREATE TABLE等),无查询结果返回,仅返回执行元数据(如受影响行数、自增 ID),是增删改的首选方法。返回值:
{meta},仅包含meta执行元数据的对象(无results)。Cloudflare+Hono 示例(创建用户/更新用户/删除用户):
typescript// 新增用户(INSERT) app.post('/users', async (c) => { const { name, email } = await c.req.json() const res = await c.env.MY_DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email).run() // res.meta 包含自增 ID 和受影响行数 return c.json({ msg: 'User created', user_id: res.meta.last_row_id }, { status: 201 }) }) // 更新用户(UPDATE) app.put('/users/:id', async (c) => { const { name } = await c.req.json() const res = await c.env.MY_DB.prepare('UPDATE users SET name = ? WHERE id = ?').bind(name, c.req.param('id')).run() // res.meta.changes 是受影响的行数,判断是否更新成功 if (res.meta.changes === 0) return c.json({ msg: 'User not found' }, { status: 404 }) return c.json({ msg: 'User updated' }) }) // 删除用户(DELETE) app.delete('/users/:id', async (c) => { const res = await c.env.MY_DB.prepare('DELETE FROM users WHERE id = ?').bind(c.req.param('id')).run() if (res.meta.changes === 0) return c.json({ msg: 'User not found' }, { status: 404 }) return c.json({ msg: 'User deleted' }) })
模块 2:事务处理方法
D1 支持完整的 ACID 事务,适合需要多步 SQL 操作原子性的场景(如「创建订单+扣减库存」,要么都成功,要么都失败),核心是 transaction() 方法创建事务对象,配合 commit()/rollback() 完成提交/回滚。
transaction()
:transaction() - 创建事务对象
- 语法:
const tx = d1Instance.transaction() - 核心作用:创建 D1 事务对象,后续可在该对象上调用
prepare/bind注册多步 SQL 操作,事务内的操作默认不会立即执行,需调用commit()提交。 - 关键:事务对象的 SQL 注册方式与 D1 实例完全一致。
commit()
:commit() - 提交事务
- 语法:
await tx.commit() - 核心作用:执行事务对象中注册的所有 SQL 操作,若全部执行成功,事务提交,数据持久化;若任意一步失败,抛出异常,需配合
try/catch调用rollback()。
rollback()
:rollback() - 回滚事务
- 语法:
await tx.rollback() - 核心作用:当事务内任意一步操作失败时,回滚所有已注册的 SQL 操作,恢复到事务执行前的数据库状态,保证数据一致性。
示例:Cloudflare+Hono 事务
Cloudflare+Hono 事务完整示例(创建用户+新增用户日志):
// 先创建用户日志表(提前执行 SQL)
// CREATE TABLE IF NOT EXISTS user_logs (
// id INTEGER PRIMARY KEY AUTOINCREMENT,
// user_id INTEGER NOT NULL,
// operation TEXT NOT NULL,
// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
// FOREIGN KEY (user_id) REFERENCES users (id)
// );
app.post('/users/with-log', async (c) => {
const { name, email } = await c.req.json()
// 1. 创建事务对象
const tx = c.env.MY_DB.transaction()
try {
// 2. 向事务中注册多步 SQL 操作(仅注册,不执行)
// 步骤1:新增用户
tx.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email)
// 步骤2:新增用户日志(获取刚新增的自增 ID,用 last_insert_rowid())
tx.prepare('INSERT INTO user_logs (user_id, operation) VALUES (last_insert_rowid(), ?)').bind('create')
// 3. 提交事务:执行所有注册的操作
const res = await tx.commit()
return c.json(
{
msg: 'User created with log',
user_id: res.meta.last_row_id
},
{ status: 201 }
)
} catch (err) {
// 4. 回滚事务:任意一步失败,恢复数据
await tx.rollback()
return c.json(
{
msg: 'Operation failed',
error: err.message
},
{ status: 500 }
)
}
})模块 3:批量操作方法
batch 适合批量执行相同结构的 SQL 语句(如批量插入多条数据),比循环调用 run() 更高效(减少数据库交互次数),核心是 prepare 预编译一次,传入多组参数批量执行。
batch()
:batch(paramsArray: any[][]) - 批量执行 SQL
语法:
prepare(sql).batch(paramsArray)核心作用:对预编译的 SQL 语句,传入二维参数数组,批量执行多组操作,每组参数对应 SQL 中的
?占位符。参数:
paramsArray为二维数组,如[[p1,p2], [p3,p4], ...],每组子数组对应一次 SQL 执行的参数。返回值:包含每组执行结果的数组,每个结果均为含
meta的对象。Cloudflare+Hono 批量操作示例(批量插入用户):
tstagsApp.post('/batch', async (c) => { // 1. 获取参数 const tags: Tag[] = await c.req.json() if (!Array.isArray(tags) || tags.length === 0) return c.json({ error: 'At least one tag is required' }, 400) // 2. 组织 SQL 语句 try { const keys = Object.keys(tags[0]) const fieldsStr = keys.join(',') const valuesStr = keys.map(() => '?').join(',') let stmt = `INSERT INTO tags (${fieldsStr}) VALUES (${valuesStr})` const stmts = tags.map((tag) => c.env.DB_JAV.prepare(stmt).bind(...Object.values(tag))) // 3. 执行数据库操作 const results = await c.env.DB_JAV.batch(stmts) // 4. 返回结果 return c.json({ success: results.every((result) => result.success), results: results }) } catch (error) { return c.json({ error: error instanceof Error ? error.message : error }, 500) } })
特性:batch() 默认开启隐式事务(Implicit Transaction):
会回滚吗? 会。
batch()默认在 隐式事务(Implicit Transaction) 中执行。如果其中任何一条语句失败,所有语句(包括之前执行成功的)都会回滚,数据库状态会恢复到执行batch()之前的状态。后续还会执行吗? 不会。 一旦遇到错误,执行立即由于异常而中断,后续尚未执行的语句会被跳过。
详细机制解析:
Cloudflare D1 的 batch() 方法不仅仅是为了减少 HTTP 请求往返(Round-trip)的性能优化,它也是 D1 实现 原子性(Atomicity) 的主要手段。
当你调用 env.DB.batch([stmt1, stmt2, stmt3]) 时,D1 的处理逻辑如下:
开启事务:D1 会自动执行
BEGIN TRANSACTION。顺序执行:按照数组顺序依次执行 SQL 语句。
错误捕获:
- 如果
stmt1成功,继续执行stmt2。 - 如果
stmt2失败: - D1 捕获错误。
- 执行
ROLLBACK(回滚stmt1的操作)。 - 向你的 Worker 抛出异常(Error)。
stmt3永远不会被执行。
- 如果
提交事务:只有当数组中所有语句都成功执行后,D1 才会执行
COMMIT,将变更持久化。
代码示例:
假设你正在使用 Hono 或原生 Worker 开发:
try {
// 准备三个语句
const stmt1 = env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice')
// 假设这是一个故意写错的 SQL,会导致执行失败
const stmt2 = env.DB.prepare('INSERT INTO non_existent_table (name) VALUES (?)').bind('Bob')
const stmt3 = env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Charlie')
// 批量执行
const results = await env.DB.batch([stmt1, stmt2, stmt3])
console.log('执行成功')
} catch (e) {
console.error('执行失败,已触发回滚')
console.error('错误信息:', e.message)
// 此时,数据库中:
// 1. Alice 不会被插入(已回滚)
// 2. Charlie 不会被尝试插入(被跳过)
}特殊情况与注意事项:
手动事务控制:虽然
batch()自带事务,但你也可以在batch内部手动写入BEGIN和COMMIT语句,但这通常是不必要的,且容易导致嵌套事务逻辑混乱。建议直接依赖batch()的原子性。D1 的限制:请注意 D1 的
batch()有大小限制(例如 SQL 语句的复杂度和参数大小),如果超限可能会在发送请求前就报错。非事务性批量执行:如果你希望“能执行多少是多少”(即前一个失败不影响后一个,也不回滚前一个),你不能使用
batch()。你需要自己在代码中用for循环配合await逐个执行stmt.run(),并手动用try-catch包裹每一个执行操作。但这会导致大量的网络 IO,性能会非常差。
一句话总结: batch() 是全有或全无(All-or-Nothing)的操作,你可以放心地将其作为事务处理机制使用。
D1 执行结果对象的常用属性
调用 D1 执行方法(all/first/get/run)后,会返回执行结果对象,其中包含查询数据和执行元数据两大核心属性,是开发中判断执行结果、获取关键信息的依据。
meta
通用核心属性::meta(执行元数据)
所有执行方法的返回结果中均包含 meta 属性,记录 SQL 执行的核心元数据,无查询结果时(如 run()),结果对象仅含 meta。 meta 是一个对象,高频属性如下:
| 属性名 | 类型 | 核心作用 | 适用场景 |
|---|---|---|---|
changes | number | 受 SQL 操作影响的行数(如 UPDATE/DELETE 改了多少行,INSERT 为 1) | 增/删/改后判断是否执行成功 |
last_row_id | number | 执行 INSERT 后,自增主键(PRIMARY KEY AUTOINCREMENT)的最新 ID | 新增数据后获取记录 ID |
duration | number | SQL 执行耗时(毫秒) | 性能调试、优化 |
rows_read | number | 执行查询时读取的行数 | 查詢性能优化 |
rows_written | number | 执行写操作时写入的行数 | 写操作性能优化 |
示例:获取 meta 中的关键信息
const res = await c.env.MY_DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('Bob', 'bob@cf.com').run()
console.log(res.meta.changes) // 1(插入1行)
console.log(res.meta.last_row_id) // 3(假设最新自增ID为3)
console.log(res.meta.duration) // 0.5(执行耗时0.5毫秒)results
查询类方法专属属性::results
仅 all()/first() 方法的返回结果包含 results 属性,存储 SQL 查询的数据结果:
all().results:数组,包含所有符合条件的查询结果,每个元素为一条记录的对象(键为列名,值为列值);first().results:对象,仅包含第一条查询结果(部分版本中first()直接返回结果对象,无需取results,兼容两种写法)。
示例:results 数据结构
const res = await c.env.MY_DB.prepare('SELECT * FROM users LIMIT 2').all()
console.log(res.results)
// 输出:
// [
// { id: 1, name: 'Alice', email: 'alice@cf.com', created_at: '2026-01-24T00:00:00Z' },
// { id: 2, name: 'Bob', email: 'bob@cf.com', created_at: '2026-01-24T00:01:00Z' }
// ]D1 开发核心注意事项
D1 开发核心注意事项(与方法使用强相关):
- 参数绑定是强制规范:严禁直接拼接 SQL 字符串(如
SELECT * FROM users WHERE id = ${id}),必须使用prepare + bind,防止 SQL 注入; - 执行方法的选型原则:
- 查多条 →
all();查单条 →first();查单个值 →get(); - 增/删/改/建表 →
run();批量增/删/改 →batch();
- 查多条 →
- 事务的使用场景:多步 SQL 操作需原子性时使用,避免单步操作开事务(影响性能);
last_insert_rowid():SQL 中可直接使用该函数获取当前连接中最后一次 INSERT 的自增 ID,适合事务中关联多表操作;- 数据类型兼容:D1 基于 SQLite,无严格的类型校验(如整数列可存字符串),建议在代码中做类型校验,保证数据一致性。
R2
什么是 R2
什么是 Cloudflare R2?:
R2 是 Cloudflare 打造的无出口带宽费对象存储服务,兼容亚马逊 S3 核心 API,数据存储在 Cloudflare 全球数据中心集群,同时可通过 Cloudflare 边缘网络实现全球内容分发,无需额外支付数据从存储到用户的出口流量费(这是与 AWS S3、阿里云 OSS 等传统对象存储的核心差异)。
核心定位:
填补 Cloudflare 生态非结构化、大文件、持久化存储的空白:
- KV 适合小体积、高频读写、键值型数据(单值最大 25MB);
- D1 适合结构化、关联型SQL 数据;
- Durable Objects 适合细粒度状态、分布式锁;
- R2 适合非结构化大文件(如图片、视频、静态资源、备份文件、用户上传文件,单文件最大 5TB)。
简单来说:R2 是 Cloudflare 生态的「S3 平替」,无出口费、兼容 S3 API、无缝集成边缘运行时,是边缘应用中存储/分发非结构化文件的首选方案。
R2 核心优势
R2 核心优势(对比传统对象存储/Cloudflare 其他存储):
核心亮点:无出口带宽费:
传统对象存储(S3/OSS)的出口带宽费是核心成本(数据从存储服务器传输到用户的流量费),而 R2 完全免除这一费用——无论多少数据从 R2 分发到全球用户,均不收取出口流量费,仅按存储容量和操作次数计费,大幅降低大文件分发成本。
与传统对象存储的核心对比:
| 特性维度 | Cloudflare R2 | AWS S3/阿里云 OSS(传统对象存储) |
|---|---|---|
| 出口带宽费 | 完全免费 | 按流量计费(跨区域/公网出口昂贵) |
| S3 API 兼容 | 兼容核心 S3 API(可直接迁移) | 原生 S3 API/自研 API |
| 边缘分发能力 | 深度整合 Cloudflare 边缘网络,就近缓存分发 | 需搭配独立 CDN(如 Cloudfront/CDN 加速) |
| 生态集成 | 无缝衔接 Workers/Hono/D1(本地调用,无网络开销) | 需通过公网/内网 API 调用,有网络延迟 |
| 存储成本 | 更低(尤其是大流量分发场景) | 基础存储成本低,流量成本高 |
与 Cloudflare 其他存储的适用场景区分:
| 存储服务 | 存储类型 | 单文件/值上限 | 核心适用场景 | 搭配 R2 用法 |
|---|---|---|---|---|
| R2 | 对象存储 | 5TB | 图片/视频/静态资源/用户上传文件 | 存储实际文件 |
| KV | 键值存储 | 25MB/值 | 高频读写的小数据、文件元数据缓存 | 缓存 R2 文件的 URL/大小/哈希等元数据 |
| D1 | 关系型数据库 | 无(按行存储) | 文件关联的结构化数据(如上传记录) | 存储文件的用户ID/上传时间/分类等 |
| Durable Objects | 状态存储 | 无(内存+持久化) | 大文件上传的断点续传/分布式锁 | 控制 R2 并发上传/文件操作 |
R2 其他核心优势:
- S3 无缝迁移:兼容 S3 核心 API(
GetObject/PutObject/ListObjects等),原有基于 S3 的代码可几乎零修改迁移到 R2; - 边缘缓存加速:R2 文件可通过 Cloudflare CDN 做边缘缓存,全球用户就近访问,延迟低至毫秒级;
- 无最小存储期限:无 S3 那样的「最小存储 30 天」限制,临时存储文件也无需支付额外费用;
- 多存储层级:支持「即时访问」(默认,低延迟)和「低频访问」(更低存储成本,适合冷数据/备份);
- Workers 本地调用:R2 与 Cloudflare Workers 运行在同一节点,代码中调用 R2 无跨网络请求开销,性能拉满。
R2 前置准备
R2 前置准备:
与 Cloudflare 其他服务一致,R2 的管理和开发依赖 Wrangler,同时需完成 Cloudflare 账户授权,前置步骤如下:
- 安装/升级 Wrangler 至 v3.0+(R2 核心功能在 v3 完善):bash
npm install -g wrangler # 验证版本 wrangler -v # 输出 >= 3.0.0 即可 - 登录 Cloudflare 账户(授权 Wrangler 访问 R2 资源):bash
wrangler login - (可选)熟悉 S3 基础 API:R2 兼容 S3 核心 API,了解基础用法可降低学习成本(无 S3 基础也可直接学 R2 原生方法)。
R2 常用 Wrangler 命令
R2 基础操作(Wrangler 命令行+配置绑定):
R2 的存储桶(Bucket) 是文件存储的基本单位(类似文件夹,所有文件都在存储桶中),先通过 Wrangler 完成存储桶的创建、管理,并配置到 wrangler.toml 中,才能在代码中访问。
核心概念:R2 存储桶(Bucket):
- 存储桶是 R2 的顶级命名空间,全球唯一命名(不可与其他 Cloudflare 用户重复);
- 存储桶包含对象(Object):即实际存储的文件,每个对象有唯一的键(Key)(如
images/avatar.png); - 存储桶支持前缀(Prefix):通过键的路径分隔符(
/)实现文件分类(如images/前缀下的所有图片)。
模块 1:Wrangler 管理 R2 存储桶
所有存储桶的创建、查询、删除均通过 wrangler r2 命令完成,以下是开发高频用法:
创建 R2 存储桶
创建 R2 存储桶(核心):
# 交互式创建(推荐,自动校验命名唯一性)
wrangler r2 bucket create <BUCKET_NAME> # 如 wrangler r2 bucket create my-first-r2-bucket
# 非交互式创建(指定存储区域,可选:us/europe/apac)
wrangler r2 bucket create <BUCKET_NAME> --location us- 执行成功后,终端会提示存储桶创建成功,并可直接用于代码绑定;
- 命名规则:仅含小写字母、数字、连字符(
-),开头/结尾不能为连字符,长度 3-63 位。
列出所有 R2 存储桶
列出所有 R2 存储桶:
wrangler r2 bucket list- 输出账户下所有存储桶的名称、创建时间、存储区域,方便核对。
删除 R2 存储桶
删除 R2 存储桶(谨慎):
wrangler r2 bucket delete <BUCKET_NAME>- 注意:删除存储桶前需先删除桶内所有文件,否则会执行失败。
查看存储桶内文件
查看存储桶内文件(快速调试):
# 列出存储桶内所有文件
wrangler r2 object list <BUCKET_NAME>
# 按前缀过滤文件(如仅列出 images/ 下的文件)
wrangler r2 object list <BUCKET_NAME> --prefix images/
# 显示文件详细信息(大小、哈希、修改时间)
wrangler r2 object list <BUCKET_NAME> --details模块 2:将 R2 绑定到 Workers/Hono 项目
需在项目的 wrangler.toml 中配置 R2 绑定,才能在代码中通过 c.env.<BINDING_NAME> 访问存储桶实例,这是代码操作 R2 的前提。
配置 wrangler.toml
配置 :wrangler.toml(核心)
在项目根目录的 wrangler.toml 中添加 [[r2_buckets]] 节点,配置如下:
# 项目基础配置
name = "r2-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"
# R2 存储桶绑定(关键!)
[[r2_buckets]]
binding = "MY_R2_BUCKET" # 代码中通过 c.env.MY_R2_BUCKET 访问
bucket_name = "my-first-r2-bucket" # 替换为你的 R2 存储桶名称- 一个项目可绑定多个 R2 存储桶,只需添加多个
[[r2_buckets]]节点即可; - 绑定名(
binding)建议大写,符合 Cloudflare 生态命名规范。
R2 常用方法
R2 存储桶实例的核心常用方法(代码层操作):
配置绑定后,在 Workers/Hono 代码中通过 c.env.<BINDING_NAME> 访问的R2 存储桶实例(如 c.env.MY_R2_BUCKET)是操作 R2 的唯一入口,所有文件的上传、下载、删除、查询均通过该实例的方法实现。
核心前提:
- 所有 R2 方法均为异步方法,需配合
async/await使用; - R2 文件的键(Key) 是唯一标识(如
avatar.png、images/2026/01/pic.jpg),支持路径分隔符\//,建议统一用/做分类; - 方法参数支持自定义元数据、缓存控制、内容类型等,适配不同业务场景;
- 所有方法均基于 Web Standard API 实现,无 Node.js 依赖,完美适配 Cloudflare 边缘运行时。
R2 存储桶实例的高频方法(按功能分类):
以下方法为 Cloudflare 封装的原生 R2 方法(比 S3 API 更简洁,推荐在 Workers/Hono 中使用),每个方法包含语法、核心作用、Hono 场景示例,可直接落地。
模块 1:文件上传/更新
模块 1:文件上传/更新(:put)
向 R2 存储桶中上传新文件或覆盖已有文件(相同键会直接覆盖),支持纯文本、二进制、流、FormData 等多种上传方式,是文件操作的核心方法。
- 语法:
await bucket.put(key, data, [options]) - 参数:
key:字符串,文件唯一键(如images/avatar.png);data:上传数据,支持string/ArrayBuffer/Blob/ReadableStream/FormData;options:可选对象,配置文件元数据,高频属性:httpMetadata:HTTP 元数据,如contentType(内容类型,如image/png)、cacheControl(缓存控制,如public, max-age=31536000);customMetadata:自定义元数据(如{ author: 'cf', uploadTime: '2026-01-24' });contentLength:文件长度(数字,可选,自动推导)。
- 返回值:R2 对象实例(包含文件键、大小、哈希、元数据等信息)。
- Hono 示例(纯文本上传/二进制上传/FormData 表单上传):
import { Hono } from 'hono'
const app = new Hono()
// 1. 上传纯文本文件
app.post('/r2/upload/text', async (c) => {
const bucket = c.env.MY_R2_BUCKET
// 上传纯文本,指定内容类型和缓存控制
const object = await bucket.put('test.txt', 'Hello Cloudflare R2! 🚀', {
httpMetadata: {
contentType: 'text/plain',
cacheControl: 'public, max-age=86400'
},
customMetadata: { author: 'hono-r2' }
})
return c.json({
msg: 'Text file uploaded',
key: object.key,
size: object.size,
etag: object.etag
})
})
// 2. 上传二进制文件(如前端传的图片/视频)
app.post('/r2/upload/bin', async (c) => {
const bucket = c.env.MY_R2_BUCKET
// 读取请求体的二进制数据
const data = await c.req.blob()
// 自定义文件键(如 random + 后缀)
const key = `images/${Date.now()}.png`
// 上传二进制,自动识别内容类型
const object = await bucket.put(key, data, {
httpMetadata: { contentType: data.type }
})
return c.json({ msg: 'Binary file uploaded', key: object.key })
})
// 3. 处理 FormData 表单上传(前端常用)
app.post('/r2/upload/form', async (c) => {
const bucket = c.env.MY_R2_BUCKET
const form = await c.req.formData()
// 获取表单中的文件(前端 input name="file")
const file = form.get('file') as File
if (!file) return c.json({ msg: 'No file uploaded' }, { status: 400 })
// 上传文件,使用原文件名作为键
const object = await bucket.put(file.name, file, {
httpMetadata: {
contentType: file.type,
cacheControl: 'public, max-age=31536000'
}
})
return c.json({ msg: 'Form file uploaded', key: object.key })
})模块 2:文件下载/读取
模块 2:文件下载/读取(:get)
从 R2 存储桶中读取指定键的文件,支持直接返回给客户端(作为响应)或在代码中处理文件内容(如解析、转码),是文件分发的核心方法。
- 语法:
await bucket.get(key, [options]) - 参数:
key:字符串,文件唯一键;options:可选对象,如range(字节范围,用于断点续传)、onlyIf(条件下载,如ifNoneMatch)。
- 返回值:R2 对象实例(文件存在)/
null(文件不存在)。 - 关键技巧:通过
new Response(object.body, { headers: object.httpMetadata.headers })可直接将 R2 文件作为 HTTP 响应返回给客户端,自动携带所有元数据。 - Hono 示例(文件直接下载/代码中读取文件内容):
// 1. 直接下载文件(核心用法,前端访问该接口即可下载/预览)
app.get('/r2/download/:key*', async (c) => {
const bucket = c.env.MY_R2_BUCKET
// 获取文件键(支持路径,如 /r2/download/images/avatar.png)
const key = c.req.param('key*')
const object = await bucket.get(key)
if (!object) return c.json({ msg: 'File not found' }, { status: 404 })
// 直接返回 R2 文件作为响应,自动携带内容类型、缓存控制等
return new Response(object.body, {
headers: {
...object.httpMetadata.headers,
ETag: object.etag,
'Content-Length': object.size.toString()
}
})
})
// 2. 代码中读取文件内容(如解析文本文件)
app.get('/r2/read/:key', async (c) => {
const bucket = c.env.MY_R2_BUCKET
const key = c.req.param('key')
const object = await bucket.get(key)
if (!object) return c.json({ msg: 'File not found' }, { status: 404 })
// 读取文件内容为纯文本
const content = await object.text()
// 也可读取为二进制/Blob
// const buffer = await object.arrayBuffer()
return c.json({
key: object.key,
size: object.size,
content
})
})模块 3:文件删除
模块 3:文件删除(:delete)
删除 R2 存储桶中单个/多个文件,支持批量删除,适合文件管理场景。
- 语法1(单文件删除):
await bucket.delete(key) - 语法2(批量文件删除):
await bucket.delete([key1, key2, key3]) - 参数:单个键字符串 / 键数组(批量删除);
- 返回值:
void(无返回值,删除不存在的文件不会报错)。 - Hono 示例(单文件删除/批量文件删除):
// 1. 单文件删除
app.delete('/r2/delete/:key', async (c) => {
const bucket = c.env.MY_R2_BUCKET
const key = c.req.param('key')
await bucket.delete(key)
return c.json({ msg: 'File deleted successfully' })
})
// 2. 批量文件删除(接收键数组)
app.post('/r2/delete/batch', async (c) => {
const bucket = c.env.MY_R2_BUCKET
const { keys } = await c.req.json()
if (!Array.isArray(keys) || keys.length === 0) {
return c.json({ msg: 'No keys provided' }, { status: 400 })
}
await bucket.delete(keys)
return c.json({ msg: `Batched delete ${keys.length} files` })
})模块 4:列出桶内文件
模块 4:列出桶内文件(:list)
查询 R2 存储桶中的文件列表,支持前缀过滤、分页、按前缀分组,适合实现「文件管理器」「文件列表查询」等功能。
- 语法:
await bucket.list([options]) - 参数:
options为核心配置对象,高频属性:prefix:字符串,按前缀过滤(如images/,仅列出该前缀下的文件);delimiter:字符串,分隔符(如/,按路径分组,不显示子目录下的文件);limit:数字,分页大小(最大 1000);cursor:字符串,分页游标(用于下一页查询,从上次返回的truncated和cursor获取)。
- 返回值:对象,包含
objects(文件数组)、prefixes(前缀分组数组)、truncated(是否还有更多文件)、cursor(下一页游标)。 - Hono 示例(列出所有文件/按前缀过滤/分页查询):
// 列出文件(支持前缀、分页)
app.get('/r2/list', async (c) => {
const bucket = c.env.MY_R2_BUCKET
// 获取查询参数:前缀、分页大小、游标
const prefix = c.req.query('prefix') || ''
const limit = Number(c.req.query('limit')) || 10
const cursor = c.req.query('cursor') || undefined
const result = await bucket.list({
prefix,
limit,
cursor,
delimiter: '/' // 按 / 分组,显示目录结构
})
// 整理返回数据(仅保留核心信息)
const files = result.objects.map((obj) => ({
key: obj.key,
size: obj.size,
etag: obj.etag,
uploadTime: obj.uploaded.toISOString(),
contentType: obj.httpMetadata.contentType
}))
return c.json({
files,
prefixes: result.prefixes, // 前缀分组(如 images/)
truncated: result.truncated, // 是否有更多文件
nextCursor: result.cursor // 下一页游标
})
})模块 5:查询文件元数据
模块 5:查询文件元数据(:head)
仅查询文件的元数据(不下载文件内容),比 get 更轻量,适合验证文件是否存在、获取文件大小/类型等场景,减少带宽消耗。
- 语法:
await bucket.head(key) - 参数:文件唯一键;
- 返回值:R2 对象实例(仅含元数据,无文件体)/
null(文件不存在)。 - Hono 示例:
app.get('/r2/meta/:key', async (c) => {
const bucket = c.env.MY_R2_BUCKET
const key = c.req.param('key')
const object = await bucket.head(key)
if (!object) return c.json({ msg: 'File not found' }, { status: 404 })
return c.json({
key: object.key,
size: object.size,
etag: object.etag,
uploadTime: object.uploaded.toISOString(),
contentType: object.httpMetadata.contentType,
cacheControl: object.httpMetadata.cacheControl,
customMetadata: object.customMetadata
})
})实战:Hono + R2 实现完整的文件管理服务
实战:Hono + R2 实现完整的文件管理服务:
以下是端到端的 Hono + R2 实战示例,实现文件上传、下载、删除、列表查询、元数据查询五大核心功能,可直接复制到项目中使用,只需替换 wrangler.toml 中的 R2 绑定名和存储桶名称。
步骤 1:初始化 Hono + R2 项目:
# 初始化 Workers 项目
wrangler init r2-hono-demo
cd r2-hono-demo
# 安装 Hono
npm install hono
# 创建 R2 存储桶(替换为你的存储桶名称)
wrangler r2 bucket create my-r2-hono-bucket步骤 2:配置 wrangler.toml:
name = "r2-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"
node_compat = false # 关闭 Node.js 兼容,纯边缘运行时
# R2 存储桶绑定
[[r2_buckets]]
binding = "MY_R2_BUCKET"
bucket_name = "my-r2-hono-bucket" # 替换为你的存储桶名称步骤 3:编写核心代码(src/index.ts):
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
// 开启 CORS(解决前端跨域上传/下载)
app.use(
'*',
cors({
origin: '*', // 生产环境指定具体域名
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization']
})
)
// 全局获取 R2 存储桶实例
const getBucket = (c: any) => c.env.MY_R2_BUCKET
// 1. 表单上传文件(前端常用)
app.post('/upload', async (c) => {
const bucket = getBucket(c)
const form = await c.req.formData()
const file = form.get('file') as File
if (!file) return c.json({ code: 400, msg: '请选择上传文件' }, { status: 400 })
// 自定义文件键:前缀 + 时间戳 + 原文件名(避免重复)
const key = `uploads/${Date.now()}_${file.name.replace(/\s+/g, '_')}`
try {
const object = await bucket.put(key, file, {
httpMetadata: {
contentType: file.type,
cacheControl: 'public, max-age=31536000' // 缓存1年
},
customMetadata: { uploader: 'hono-r2' }
})
return c.json({
code: 200,
msg: '上传成功',
data: {
key: object.key,
size: object.size,
url: `/download/${object.key}` // 下载地址
}
})
} catch (err) {
return c.json({ code: 500, msg: '上传失败', error: (err as Error).message }, { status: 500 })
}
})
// 2. 文件下载/预览(支持路径)
app.get('/download/:key*', async (c) => {
const bucket = getBucket(c)
const key = c.req.param('key*')
const object = await bucket.get(key)
if (!object) return c.json({ code: 404, msg: '文件不存在' }, { status: 404 })
// 直接返回文件响应
return new Response(object.body, {
headers: {
...object.httpMetadata.headers,
ETag: object.etag,
'Content-Length': object.size.toString(),
'Content-Disposition': `inline; filename="${encodeURIComponent(object.key.split('/').pop()!)}"`
}
})
})
// 3. 删除文件
app.delete('/delete/:key', async (c) => {
const bucket = getBucket(c)
const key = c.req.param('key')
try {
await bucket.delete(key)
return c.json({ code: 200, msg: '删除成功' })
} catch (err) {
return c.json({ code: 500, msg: '删除失败', error: (err as Error).message }, { status: 500 })
}
})
// 4. 列出文件(支持前缀、分页)
app.get('/list', async (c) => {
const bucket = getBucket(c)
const prefix = c.req.query('prefix') || 'uploads/'
const limit = Number(c.req.query('limit')) || 20
const cursor = c.req.query('cursor') || undefined
const result = await bucket.list({ prefix, limit, cursor, delimiter: '/' })
const files = result.objects.map((obj) => ({
key: obj.key,
size: obj.size,
uploadTime: obj.uploaded.toISOString(),
contentType: obj.httpMetadata.contentType,
url: `/download/${obj.key}`
}))
return c.json({
code: 200,
data: {
files,
prefixes: result.prefixes,
hasMore: result.truncated,
nextCursor: result.cursor
}
})
})
// 5. 查询文件元数据
app.get('/meta/:key', async (c) => {
const bucket = getBucket(c)
const key = c.req.param('key')
const object = await bucket.head(key)
if (!object) return c.json({ code: 404, msg: '文件不存在' }, { status: 404 })
return c.json({
code: 200,
data: {
key: object.key,
size: object.size,
etag: object.etag,
uploadTime: object.uploaded.toISOString(),
contentType: object.httpMetadata.contentType,
cacheControl: object.httpMetadata.cacheControl,
customMetadata: object.customMetadata
}
})
})
// 导出 Workers 处理函数
export default app.fetch步骤 4:本地调试与线上部署:
# 本地调试(启动开发服务器,默认 8787 端口)
wrangler dev
# 部署到 Cloudflare 线上
wrangler deploy步骤 5:前端简单测试(HTML 表单上传):
创建 index.html,直接打开即可测试上传功能:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>R2 + Hono 文件上传</title>
</head>
<body>
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" accept="*" />
<button type="submit">上传文件</button>
</form>
<script>
const form = document.getElementById('uploadForm')
form.onsubmit = async (e) => {
e.preventDefault()
const formData = new FormData(form)
const res = await fetch('http://localhost:8787/upload', {
method: 'POST',
body: formData
})
const data = await res.json()
console.log('上传结果:', data)
if (data.code === 200) {
alert(`上传成功,下载地址:${data.data.url}`)
}
}
</script>
</body>
</html>R2 进阶技巧
结合 Cloudflare 自定义域名分发 R2 文件
结合 Cloudflare 自定义域名分发 R2 文件:
将 R2 绑定到 Cloudflare 自定义域名(如 cdn.xxx.com),通过 https://cdn.xxx.com/avatar.png 直接访问文件,替代 Workers 路由,提升分发性能:
- 操作步骤:Cloudflare 仪表盘 → 你的域名 → Workers 路由 → 添加路由
cdn.xxx.com/*→ 绑定到你的 R2 存储桶。
设置 R2 存储桶 CORS 规则
设置 R2 存储桶 CORS 规则:
解决前端直接访问 R2 文件的跨域问题(无需 Workers 开启 CORS):
- Cloudflare 仪表盘 → R2 → 你的存储桶 → 设置 → CORS 规则 → 添加规则,示例:json
[ { "AllowedOrigins": ["*"], "AllowedMethods": ["GET", "PUT", "POST", "DELETE"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag", "Content-Length"] } ]
配置 R2 生命周期规则
配置 R2 生命周期规则(自动清理文件):
对临时文件/日志文件,设置生命周期规则自动删除,避免手动维护:
- Cloudflare 仪表盘 → R2 → 你的存储桶 → 设置 → 生命周期规则 → 添加规则:
- 示例1:删除 30 天未访问的文件;
- 示例2:删除前缀为
temp/的所有文件; - 示例3:将 90 天未访问的文件转为「低频访问」存储(降低成本)。
断点续传
断点续传(大文件上传):
利用 R2 的 put 方法支持ReadableStream和 get 方法的 range 参数,结合 Durable Objects 实现大文件断点续传,适合上传 GB 级别的视频/压缩包。
文件元数据缓存
文件元数据缓存(结合 KV):
将 R2 文件的元数据(键、大小、URL、类型)缓存到 Cloudflare KV 中,替代频繁调用 list/head 方法,提升查询性能:
- 上传文件时:同时将元数据写入 KV;
- 查询文件列表时:先从 KV 读取,再按需从 R2 同步。
R2 开发注意事项
存储桶与文件键命名规则
存储桶与文件键命名规则:
- 存储桶:全球唯一,仅含小写字母、数字、连字符,3-63 位;
- 文件键:支持 UTF-8 字符,建议避免空格/特殊字符(可替换为
_),通过前缀(如images//uploads/)做分类。
文件大小限制
文件大小限制:
- 单文件最大上传:5TB;
- Workers 运行时单次请求内存限制:128MB/256MB,上传/下载超大文件时需使用流(Stream),避免加载整个文件到内存。
权限控制
权限控制:
- 公共访问:默认 R2 存储桶为私有,需通过 Workers/Hono 做权限控制(如添加 Token 认证、用户鉴权);
- 私有文件:在 Workers 代码中添加鉴权逻辑(如验证
Authorization请求头),仅授权用户可访问/下载。
计费规则
计费规则(免费版/付费版):
- 免费版配额:10GB 存储、100 万次读操作、10 万次写操作、10GB 数据处理,满足小型应用开发;
- 付费版:按实际使用量计费,存储费≈$0.015/GB/月,操作费远低于 S3,无出口带宽费(核心优势)。
S3 API 兼容使用
S3 API 兼容使用:
若需使用 S3 原生 API 操作 R2(如通过 AWS CLI、S3 客户端),可在 Cloudflare 仪表盘获取 R2 的S3 兼容端点、Access Key、Secret Key,配置后即可无缝使用。
R2 最佳实践与适用场景
核心适用场景
核心适用场景:
- 静态资源存储与分发:图片、视频、CSS/JS、字体文件等,结合 Cloudflare 边缘缓存实现全球低延迟分发;
- 用户上传文件存储:前端表单上传的头像、附件、文档,通过 Hono/Workers 做上传校验/权限控制;
- 大文件备份/存储:备份文件、日志文件、数据库快照,无出口费,适合跨云分发;
- 边缘数据处理:通过 Workers 读取 R2 中的文件,在边缘做实时处理(如图片压缩、文本解析、视频转码),处理后再写回 R2。
Cloudflare 生态整合最佳实践
Cloudflare 生态整合最佳实践:
- R2 + Hono/Workers:实现文件的上传/下载/管理/鉴权,打造边缘文件服务;
- R2 + KV:KV 缓存文件元数据,R2 存储实际文件,提升查询性能;
- R2 + D1:D1 存储文件的结构化关联数据(如用户上传记录、文件分类、访问统计),R2 存储文件本体;
- R2 + Durable Objects:实现大文件断点续传、分布式锁、并发上传控制;
- R2 + Pages:为 Cloudflare Pages 静态站点提供文件上传/存储能力(如博客的图片上传、静态站的附件管理)。
KV
什么是 KV
什么是 Cloudflare KV:
Cloudflare KV(Key-Value)是无服务器、全球分布式的边缘键值存储,数据会同步部署到 Cloudflare 全球 300+ 边缘数据中心,用户请求会被路由到最近的边缘节点获取数据,延迟低至毫秒级。
KV 采用简单的键值对模型,无复杂的查询语法,API 极简,同时支持键过期(TTL)、自定义元数据、批量操作,核心定位是解决边缘应用中小体积、高频访问、无需关联查询的数据存储需求。
核心定位:Cloudflare 生态的「基础存储层」:
KV 是 Cloudflare 最早推出的存储服务,填补了边缘高频小数据存储的空白,与其他 Cloudflare 存储服务形成明确的场景互补(这是使用 KV 的核心前提,避免用错场景):
| 存储服务 | 核心类型 | 数据体积限制 | 核心优势 | 核心适用场景 |
|---|---|---|---|---|
| KV | 键值存储 | 单值最大 25MB,键≤512B | 全球边缘、毫秒级读写、高可用 | 配置存储、接口缓存、会话信息、高频小数据、元数据缓存 |
| D1 | 关系型数据库 | 无(按行存储) | SQL 关联查询、事务 | 结构化/关联型数据(用户信息、订单、文件记录) |
| R2 | 对象存储 | 单文件最大 5TB | 无出口费、S3 兼容、大文件 | 非结构化大文件(图片、视频、用户上传文件、静态资源) |
| Durable Objects | 状态存储 | 内存+持久化,无硬限 | 细粒度状态、分布式锁、强一致 | 实时状态、断点续传、并发控制、强一致性数据 |
简单来说:KV 是 Cloudflare 边缘开发的「轻量缓存/持久化二合一方案」,既可以做临时缓存(设置 TTL 自动过期),也可以做持久化存储(无 TTL 永久保存),是高频小数据场景的最优解。
KV 核心优势:
边缘原生,极致低延迟:
数据同步到 Cloudflare 全球边缘节点,用户请求就近访问,无需跨区域调用中心化数据库,平均延迟比传统云数据库低一个数量级,适合对响应速度要求高的场景(如接口缓存、实时配置)。
无服务器,免运维:
无需部署、管理服务器,无需关注扩容/缩容,Cloudflare 自动负责数据的分布式同步、容灾、高可用,开发者只需通过 API/Wrangler 操作键值对,零运维成本。
API 极简,生态无缝集成:
- 与 Cloudflare Workers/Hono/Pages 深度整合,代码中通过
c.env.<BINDING_NAME>直接访问,无网络请求开销(与运行时同节点); - 提供 HTTP API、Wrangler 命令行、仪表盘等多种操作方式,适配开发/运维不同场景;
- 所有方法基于 Web Standard API 实现,无 Node.js 依赖,完美适配边缘运行时。
支持过期时间(TTL)与自定义元数据:
- TTL(Time-To-Live):可为键值对设置过期时间,自动删除过期数据,适合缓存场景(如接口数据缓存 5 分钟),无需手动清理;
- 自定义元数据:可为每个键绑定额外的元数据(如 JSON 对象),存储键的附加信息(如数据更新时间、来源),无需单独存储。
高可用与高并发:
- 多节点容灾,单节点故障不影响服务,可用性达 99.99%;
- 支持超高并发读写,适合流量高峰场景(如电商秒杀的配置缓存、热点数据访问)。
免费额度友好:
开发测试阶段完全免费,免费版配额:1GB 存储、1000 万次读操作/月、100 万次写操作/月,满足小型边缘应用的全部需求。
KV 核心概念
使用 KV 前需理解 3 个核心概念,这是避免操作错误、合理设计键值结构的基础:
命名空间
命名空间(Namespace):
命名空间(Namespace)是 KV 键值对的「顶级容器」,所有键值对都必须属于某个命名空间,相当于 KV 的「数据库」。
- 全球唯一:每个命名空间有唯一的 ID 和 名称,账户内命名空间名称不可重复;
- 独立隔离:不同命名空间的键值对完全隔离,互不影响(如「dev_config」开发环境、「prod_config」生产环境,避免数据混淆);
- 多绑定支持:一个 Cloudflare Workers/Pages 项目可绑定多个命名空间,实现数据分类管理(如一个命名空间存缓存,一个存系统配置)。
键
键(Key):
键是 KV 中数据的唯一标识,用于读取、更新、删除对应的值,设计键时需遵循规则和最佳实践:
- 命名规则:最大长度 512 字节,支持字母、数字、特殊字符(
-/_:.~),区分大小写(如User1和user1是两个不同的键); - 最佳实践:采用分层命名(如
config/app/title、cache/api/user/123),通过分隔符/实现键的分类,方便管理和批量查询; - 唯一性:同一命名空间内键不可重复,重复写入会直接覆盖原有值。
值
值(Value):
值是 KV 中存储的实际数据,需注意大小限制和数据类型:
- 大小限制:单值最大 25MB,这是 KV 最核心的限制,超过该大小会写入失败(大文件请用 R2);
- 数据类型:原生仅支持二进制数据/字符串,如需存储 JSON/对象,需在代码中手动序列化(JSON.stringify) 和反序列化(JSON.parse);
- 编码:建议使用 UTF-8 编码存储字符串,避免乱码。
附加属性
附加属性(TTL + 元数据):
每个键值对可绑定两个附加属性,无需单独存储,提升开发效率:
- TTL:过期时间,单位为秒,设置后键值对会在指定时间后自动删除;也可设置为绝对时间戳(Unix 时间戳,秒级),适合精准过期;
- 自定义元数据:键值对的附加信息,为键值对对象(最大 16KB),如
{ "updateTime": 1735689600, "source": "api" },用于存储键的附加信息,不影响值的内容。
KV 前置准备
KV 前置准备:
与 Cloudflare 其他服务一致,KV 的管理和开发依赖 Wrangler v3.0+,同时需完成 Cloudflare 账户授权,前置步骤如下:
- 安装/升级 Wrangler 至 v3.0+:bash
npm install -g wrangler # 验证版本 wrangler -v # 输出 >= 3.0.0 即可 - 登录 Cloudflare 账户(授权 Wrangler 访问 KV 资源):bash
wrangler login
KV 常用 Wrangler 命令
KV 的命名空间管理(创建/删除/列表)主要通过 Wrangler 命令行完成,而键值对操作可通过命令行、代码、Cloudflare 仪表盘实现。核心前提:需将 KV 命名空间绑定到 Workers/Hono 项目,才能在代码中访问。
模块 1:Wrangler 管理 KV 命名空间
所有命名空间操作均通过 wrangler kv:namespace 命令完成,以下是开发中最常用的命令:
创建 KV 命名空间
创建 KV 命名空间(核心):
# 交互式创建(推荐,自动生成命名空间)
wrangler kv:namespace create <NAMESPACE_NAME> # 如 wrangler kv:namespace create my-first-kv
# 为不同环境创建命名空间(如开发/生产,推荐)
wrangler kv:namespace create <NAMESPACE_NAME> --env dev # 开发环境
wrangler kv:namespace create <NAMESPACE_NAME> --env prod # 生产环境- 执行成功后,终端会输出命名空间 ID 和绑定配置代码(必须复制到 wrangler.toml,这是代码访问 KV 的关键);
- 示例输出:
✨ Successfully created KV namespace "my-first-kv" (ID: 12345678-1234-1234-1234-1234567890ab) ⚡️ Add the following to your wrangler.toml: [[kv_namespaces]] binding = "MY_KV" # 代码中通过 c.env.MY_KV 访问 id = "12345678-1234-1234-1234-1234567890ab" # 命名空间唯一 ID # preview_id = "xxx" # 预览环境 ID,自动生成
列出所有 KV 命名空间
列出所有 KV 命名空间:
wrangler kv:namespace list- 输出账户下所有命名空间的名称、ID、环境,方便核对命名空间是否创建成功。
删除 KV 命名空间
删除 KV 命名空间(谨慎):
wrangler kv:namespace delete <NAMESPACE_NAME> --id <NAMESPACE_ID>- 注意:删除命名空间会永久删除其中所有键值对,且无法恢复,需谨慎操作。
重命名 KV 命名空间
重命名 KV 命名空间:
wrangler kv:namespace rename <OLD_NAME> <NEW_NAME> --id <NAMESPACE_ID>模块 2:将 KV 命名空间绑定到 Workers/Hono 项目
需在项目的 wrangler.toml 中添加 [[kv_namespaces]] 节点,将命名空间与项目绑定,这是代码中访问 KV 的唯一方式。
配置 wrangler.toml
配置 :wrangler.toml(核心)
将创建命名空间时输出的配置代码复制到 wrangler.toml,示例如下:
# 项目基础配置
name = "kv-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"
# KV 命名空间绑定(关键!)
[[kv_namespaces]]
binding = "MY_KV" # 代码中通过 c.env.MY_KV 访问该命名空间
id = "12345678-1234-1234-1234-1234567890ab" # 替换为你的命名空间 ID
# preview_id = "98765432-4321-4321-4321-ba0987654321" # 预览环境 ID,自动生成
# 多环境绑定(开发/生产,推荐)
[env.dev]
[[env.dev.kv_namespaces]]
binding = "MY_KV"
id = "dev-namespace-id-123"
[env.prod]
[[env.prod.kv_namespaces]]
binding = "MY_KV"
id = "prod-namespace-id-456"
# 绑定多个命名空间(数据分类)
[[kv_namespaces]]
binding = "CACHE_KV" # 缓存专用
id = "cache-namespace-id-789"
[[kv_namespaces]]
binding = "CONFIG_KV" # 配置专用
id = "config-namespace-id-000"- 绑定名(
binding)建议大写,符合 Cloudflare 生态命名规范; - 一个项目可绑定多个命名空间,实现数据的隔离管理(如缓存和配置分开);
- 多环境绑定可避免开发环境数据污染生产环境。
模块 3:Wrangler 操作 KV 键值对
模块 3:Wrangler 操作 KV 键值对(快速调试):
开发中可通过 Wrangler 命令行快速操作键值对,适合调试、初始化测试数据,无需编写代码:
# 写入/更新键值对(--ttl 可选,设置过期时间,单位秒)
wrangler kv:key put --namespace-id <NAMESPACE_ID> "config/app/title" "Cloudflare KV + Hono Demo" --ttl 3600
# 读取键值对
wrangler kv:key get --namespace-id <NAMESPACE_ID> "config/app/title"
# 删除键值对
wrangler kv:key delete --namespace-id <NAMESPACE_ID> "config/app/title"
# 列出命名空间内的所有键(--prefix 可选,按前缀过滤)
wrangler kv:key list --namespace-id <NAMESPACE_ID> --prefix "config/"KV 常用实例方法
KV 命名空间实例的核心常用方法(代码层操作):
配置绑定后,在 Workers/Hono 代码中通过 c.env.<BINDING_NAME> 访问的KV 命名空间实例(如 c.env.MY_KV)是操作 KV 的唯一入口,所有键值对的增、删、改、查、批量操作均通过该实例的方法实现。
核心前提:
- 所有 KV 方法均为异步方法,需配合
async/await使用; - KV 仅支持字符串/二进制值,存储 JSON/对象时需手动
JSON.stringify序列化,读取时JSON.parse反序列化; - 方法参数支持TTL 过期时间和自定义元数据,适配缓存、配置等不同场景;
- 所有方法基于 Web Standard API 实现,无 Node.js 依赖,完美适配 Cloudflare 边缘运行时。
KV 实例的高频方法(按功能分类,附 Hono 示例):
以下方法为 Cloudflare 封装的原生 KV 方法,是开发中 99% 场景会用到的核心方法,每个方法包含语法、核心作用、参数说明、Hono 实战示例,可直接落地使用。
模块 1:写入/更新键值对
模块 1:写入/更新键值对(:put)
向 KV 命名空间中写入新键值对或覆盖已有键值对(相同键直接覆盖),支持设置 TTL 过期时间和自定义元数据,是 KV 写入操作的核心方法。
- 语法:
await kvInstance.put(key, value, [options]) - 参数:
key:字符串,键名(遵循 512B 限制,建议分层命名);value:字符串/ArrayBuffer/Uint8Array,值(遵循 25MB 限制);options:可选配置对象,高频属性:ttl:数字,过期时间(单位:秒),设置后键值对自动过期删除;expiration:数字,绝对过期时间(Unix 时间戳,秒级),优先级高于ttl;metadata:对象,自定义元数据(最大 16KB),如{ updateTime: Date.now(), source: "hono" }。
- 返回值:
Promise<void>,无返回值,写入成功即resolve。 - Hono 示例(存储字符串/JSON 对象/设置 TTL/元数据):
import { Hono } from 'hono'
const app = new Hono()
// 全局获取 KV 实例
const getKV = (c: any) => c.env.MY_KV
// 1. 存储简单字符串(如系统配置)
app.post('/kv/put/string', async (c) => {
const kv = getKV(c)
const { key, value } = await c.req.json()
if (!key || !value) return c.json({ code: 400, msg: 'key 和 value 为必传' }, { status: 400 })
await kv.put(key, value)
return c.json({ code: 200, msg: '写入成功', data: { key, value } })
})
// 2. 存储 JSON 对象(需序列化,核心用法)
app.post('/kv/put/json', async (c) => {
const kv = getKV(c)
const { key, data } = await c.req.json() // data 为任意 JSON 对象
if (!key || !data) return c.json({ code: 400, msg: 'key 和 data 为必传' }, { status: 400 })
// 序列化 JSON 对象为字符串
const value = JSON.stringify(data)
// 设置元数据 + 5 分钟 TTL 过期
await kv.put(key, value, {
ttl: 300, // 5分钟后自动过期
metadata: {
updateTime: Date.now(),
type: 'json',
source: 'hono-api'
}
})
return c.json({ code: 200, msg: 'JSON 对象写入成功', data: { key } })
})
// 3. 存储二进制数据(如小图片/小文件,<25MB)
app.post('/kv/put/bin', async (c) => {
const kv = getKV(c)
const key = 'bin/avatar-small.png'
// 读取二进制请求体
const buffer = await c.req.arrayBuffer()
// 写入二进制数据
await kv.put(key, buffer, { metadata: { type: 'image/png' } })
return c.json({ code: 200, msg: '二进制数据写入成功', data: { key } })
})模块 2:读取键值对
模块 2:读取键值对(:get)
从 KV 命名空间中读取指定键的对应值,支持仅读取值、同时读取值+元数据,是 KV 读取操作的核心方法。
- 语法1(仅读取值):
await kvInstance.get(key, [type]) - 语法2(读取值+元数据):
await kvInstance.getWithMetadata(key, [type]) - 参数:
key:字符串,键名;type:可选,值的类型,用于自动解析,支持:'text'(默认):返回字符串;'json':自动反序列化为 JSON 对象(无需手动 JSON.parse);'arrayBuffer':返回 ArrayBuffer 二进制数据;'stream':返回 ReadableStream 流。
- 返回值:
get:返回对应类型的值,键不存在则返回 null;getWithMetadata:返回对象{ value: 对应类型的值, metadata: 自定义元数据对象 },键不存在则返回 null。
- Hono 示例(读取字符串/自动解析 JSON/读取二进制/读取值+元数据):
// 1. 读取简单字符串(默认 text 类型)
app.get('/kv/get/string/:key', async (c) => {
const kv = getKV(c)
const key = c.req.param('key')
const value = await kv.get(key)
if (value === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
return c.json({ code: 200, msg: '读取成功', data: { key, value } })
})
// 2. 读取 JSON 对象(指定 type: 'json',自动反序列化,核心用法)
app.get('/kv/get/json/:key', async (c) => {
const kv = getKV(c)
const key = c.req.param('key')
// 自动解析为 JSON 对象,无需手动 JSON.parse
const data = await kv.get(key, { type: 'json' })
if (data === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
return c.json({ code: 200, msg: '读取成功', data: { key, data } })
})
// 3. 读取二进制数据(如小图片)
app.get('/kv/get/bin/:key', async (c) => {
const kv = getKV(c)
const key = c.req.param('key')
const buffer = await kv.get(key, { type: 'arrayBuffer' })
if (buffer === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
// 直接返回二进制响应(如图片预览)
return new Response(buffer, {
headers: { 'Content-Type': 'image/png' }
})
})
// 4. 读取值 + 元数据(核心,适合获取键的附加信息)
app.get('/kv/get/meta/:key', async (c) => {
const kv = getKV(c)
const key = c.req.param('key')
const result = await kv.getWithMetadata(key, { type: 'json' })
if (result === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
return c.json({
code: 200,
msg: '读取成功',
data: {
key,
value: result.value,
metadata: result.metadata || {} // 元数据
}
})
})模块 3:删除键值对
模块 3:删除键值对(:delete)
删除 KV 命名空间中指定的键值对,删除不存在的键不会报错,无返回值,是 KV 删除操作的核心方法。
- 语法:
await kvInstance.delete(key) - 参数:
key为字符串,键名; - 返回值:
Promise<void>,无返回值,删除成功即resolve; - Hono 示例:
app.delete('/kv/delete/:key', async (c) => {
const kv = getKV(c)
const key = c.req.param('key')
await kv.delete(key) // 删除不存在的键不会报错
return c.json({ code: 200, msg: '删除成功', data: { key } })
})模块 4:列出命名空间内的键
模块 4:列出命名空间内的键(:list)
查询 KV 命名空间中的键列表,支持按前缀过滤、分页,适合实现键的批量管理、分层查询(如列出所有 config/ 前缀的配置键)。
- 语法:
await kvInstance.list([options]) - 参数:
options为可选配置对象,高频属性:prefix:字符串,按前缀过滤键(如'config/app/',仅列出该前缀下的键);limit:数字,分页大小(最大 1000,默认 1000);cursor:字符串,分页游标(用于下一页查询,从上次返回的cursor获取)。
- 返回值:对象,包含:
keys:键数组,每个元素为{ name: 键名, expiration?: 过期时间戳, metadata?: 元数据 };cursor:下一页游标,若cursor为null,表示无更多键;list_complete:布尔值,是否查询完成(true表示无更多键)。
- Hono 示例(列出所有键/按前缀过滤/分页查询):
app.get('/kv/list', async (c) => {
const kv = getKV(c)
// 获取查询参数:前缀、分页大小、游标
const prefix = c.req.query('prefix') || ''
const limit = Number(c.req.query('limit')) || 100
const cursor = c.req.query('cursor') || undefined
const result = await kv.list({ prefix, limit, cursor })
return c.json({
code: 200,
msg: '查询成功',
data: {
keys: result.keys,
cursor: result.cursor, // 下一页游标
hasMore: !result.list_complete // 是否有更多键
}
})
})模块 5:批量操作
模块 5:批量操作(:putMany / deleteMany)
KV 支持批量写入和批量删除键值对,比循环调用 put/delete 更高效(减少边缘节点与 KV 存储的交互次数),适合初始化数据、批量清理缓存等场景。
批量写入
批量写入(:putMany)
- 语法:
await kvInstance.putMany(entries, [options]) - 参数:
entries:数组,每个元素为单个键值对的配置,格式:{ key: 键名, value: 值, ttl?: 过期时间, expiration?: 绝对过期时间, metadata?: 元数据 };options:可选,全局配置(如统一设置 TTL)。
- 返回值:
Promise<void>,无返回值。
批量删除
批量删除(:deleteMany)
- 语法:
await kvInstance.deleteMany(keys) - 参数:
keys为字符串数组,包含需要删除的键名; - 返回值:
Promise<void>,无返回值。
示例:Hono 批量操作
// 批量写入键值对(初始化系统配置)
app.post('/kv/put/batch', async (c) => {
const kv = getKV(c)
// 批量写入的键值对数组
const entries = [
{ key: 'config/app/title', value: 'KV + Hono Demo', ttl: 86400 },
{
key: 'config/app/version',
value: '1.0.0',
metadata: { updateTime: Date.now() }
},
{ key: 'config/app/enable', value: 'true', ttl: 3600 }
]
// 也可接收前端传的批量数据
// const { entries } = await c.req.json()
await kv.putMany(entries)
return c.json({
code: 200,
msg: '批量写入成功',
data: { count: entries.length }
})
})
// 批量删除键值对(清理指定前缀的缓存)
app.post('/kv/delete/batch', async (c) => {
const kv = getKV(c)
const { keys } = await c.req.json() // 前端传的键数组:["cache/api/1", "cache/api/2"]
if (!Array.isArray(keys) || keys.length === 0) {
return c.json({ code: 400, msg: 'keys 为必传的数组' }, { status: 400 })
}
await kv.deleteMany(keys)
return c.json({
code: 200,
msg: '批量删除成功',
data: { count: keys.length }
})
})实战:Hono + KV 实现高频接口缓存服务
实战:Hono + KV 实现高频接口缓存服务:
KV 最经典的应用场景是接口数据缓存,通过缓存高频访问的接口数据,避免重复调用底层服务(如 D1/R2/第三方 API),提升接口响应速度,降低底层服务压力。以下是生产级别的 Hono + KV 缓存服务实战,包含缓存自动过期、缓存穿透防护、缓存键自动生成,可直接复用。
实战需求:
实现一个用户信息接口的缓存层:
- 首次访问
/api/user/:id时,从 D1 读取用户数据,同时将数据缓存到 KV(设置 5 分钟过期); - 5 分钟内再次访问该接口,直接从 KV 读取缓存数据,无需访问 D1;
- 缓存过期后,自动重新从 D1 读取并更新缓存;
- 防护缓存穿透(键不存在时,缓存空值并设置短时间过期)。
实战步骤:
步骤 1:配置 wrangler.toml:
已绑定 KV 命名空间(MY_KV)和 D1 数据库(MY_DB),参考之前的 D1/KV 配置。
步骤 2:编写核心代码(src/index.ts):
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('*', cors()) // 解决跨域
// 全局获取 KV 和 D1 实例
const getKV = (c: any) => c.env.MY_KV
const getDB = (c: any) => c.env.MY_DB
// 缓存键生成工具(避免硬编码,统一前缀)
const generateCacheKey = (prefix: string, id: string | number) => `cache/${prefix}/${id}`
// 缓存过期时间:5 分钟(300 秒)
const CACHE_TTL = 300
// 缓存穿透空值过期时间:10 秒
const EMPTY_CACHE_TTL = 10
// 从 D1 读取用户数据(底层服务)
const getUserFromDB = async (db: any, id: number) => {
const { results } = await db.prepare('SELECT * FROM users WHERE id = ?').bind(id).all()
return results.length > 0 ? results[0] : null
}
// 核心:带缓存的用户信息接口
app.get('/api/user/:id', async (c) => {
const kv = getKV(c)
const db = getDB(c)
const userId = Number(c.req.param('id'))
if (isNaN(userId)) return c.json({ code: 400, msg: '用户ID为数字' }, { status: 400 })
// 1. 生成缓存键
const cacheKey = generateCacheKey('api/user', userId)
// 2. 从 KV 读取缓存
const cacheData = await kv.get(cacheKey, { type: 'json' })
// 3. 缓存命中:直接返回缓存数据
if (cacheData !== null) {
// 区分真实数据和空值(防护缓存穿透)
if (cacheData === 'EMPTY') return c.json({ code: 404, msg: '用户不存在' }, { status: 404 })
return c.json({
code: 200,
msg: '缓存命中',
data: cacheData,
from: 'kv-cache'
})
}
// 4. 缓存未命中:从 D1 读取数据
const userData = await getUserFromDB(db, userId)
// 5. 处理数据:存在则缓存,不存在则缓存空值(防护穿透)
if (userData) {
// 缓存真实数据,设置 5 分钟过期
await kv.put(cacheKey, JSON.stringify(userData), { ttl: CACHE_TTL })
return c.json({
code: 200,
msg: '缓存未命中,从数据库读取',
data: userData,
from: 'd1-db'
})
} else {
// 缓存空值(标记为 EMPTY),设置 10 秒过期,避免重复查询不存在的用户
await kv.put(cacheKey, 'EMPTY', { ttl: EMPTY_CACHE_TTL })
return c.json({ code: 404, msg: '用户不存在' }, { status: 404 })
}
})
// 手动刷新指定用户的缓存
app.post('/api/cache/refresh/user/:id', async (c) => {
const kv = getKV(c)
const db = getDB(c)
const userId = Number(c.req.param('id'))
if (isNaN(userId)) return c.json({ code: 400, msg: '用户ID为数字' }, { status: 400 })
const cacheKey = generateCacheKey('api/user', userId)
const userData = await getUserFromDB(db, userId)
if (userData) {
await kv.put(cacheKey, JSON.stringify(userData), { ttl: CACHE_TTL })
return c.json({ code: 200, msg: '缓存刷新成功', data: userData })
} else {
await kv.delete(cacheKey)
return c.json({ code: 404, msg: '用户不存在,已删除缓存' }, { status: 404 })
}
})
// 批量清理用户缓存
app.delete('/api/cache/clear/user', async (c) => {
const kv = getKV(c)
// 列出所有用户缓存键
const { keys } = await kv.list({ prefix: 'cache/api/user/' })
const cacheKeys = keys.map((key) => key.name)
if (cacheKeys.length > 0) await kv.deleteMany(cacheKeys)
return c.json({
code: 200,
msg: '用户缓存清理成功',
data: { count: cacheKeys.length }
})
})
export default app.fetch步骤 3:本地调试与部署:
# 本地调试
wrangler dev
# 线上部署
wrangler deploy实战效果:
- 首次访问
http://localhost:8787/api/user/1→ 缓存未命中,从 D1 读取并缓存; - 5 分钟内再次访问 → 缓存命中,直接从 KV 返回,响应速度提升至毫秒级;
- 访问不存在的用户
http://localhost:8787/api/user/999→ 缓存空值 10 秒,10 秒内重复访问不会查询 D1; - 调用刷新接口 → 手动更新缓存,适用于用户数据修改后同步缓存。
KV 进阶技巧
KV 进阶技巧:
合理设计键的分层结构:
采用 前缀/模块/标识 的分层命名规则,如 cache/api/user/123、config/app/title、session/user/456,优势:
- 方便通过
list方法按前缀过滤,实现批量管理; - 键的含义清晰,易于维护;
- 便于后续按模块拆分到不同的命名空间。
利用 TTL 实现多级缓存:
为不同类型的数据设置不同的 TTL,实现多级缓存:
- 热点数据:TTL 设为 5-10 分钟,高频刷新;
- 静态配置:TTL 设为 1-24 小时,减少更新频率;
- 临时数据:TTL 设为几秒到几分钟,自动清理。
防护缓存穿透/缓存雪崩:
- 缓存穿透:查询不存在的键时,缓存空值并设置短时间 TTL(如 10 秒),避免重复查询底层服务;
- 缓存雪崩:为不同的键设置随机 TTL(如 300±10 秒),避免大量键同时过期,导致底层服务压力骤增。
结合 Cloudflare 缓存规则:
将 KV 缓存的接口与 Cloudflare 全局缓存规则结合,实现边缘双层缓存:
- KV 作为应用层缓存,存储个性化/动态数据;
- Cloudflare 全局缓存作为CDN 层缓存,存储静态/公共数据;
- 进一步提升响应速度,降低 KV 读取次数。
使用元数据做数据版本控制:
为键的元数据添加版本号(如 { version: 1, updateTime: Date.now() }),实现数据的版本控制:
- 读取数据时同时检查版本号,判断是否需要更新;
- 批量更新时,只需修改版本号元数据,无需遍历所有键。
多命名空间隔离数据:
将缓存、配置、会话等不同类型的数据放到不同的命名空间,优势:
- 数据隔离,避免误操作删除重要数据;
- 可针对不同命名空间设置不同的清理策略;
- 便于多环境管理(如开发/生产的缓存命名空间分开)。
KV 开发注意事项
KV 开发注意事项:
严格遵守大小限制:
- 键最大 512 字节,值最大 25MB,超过会操作失败;
- 若值超过 25MB,请改用 R2 存储,KV 仅存储 R2 文件的键/URL 作为元数据。
注意数据类型的序列化/反序列化:
KV 仅支持字符串/二进制,存储 JSON/对象/数字时,必须手动:
- 写入:
JSON.stringify(data)序列化; - 读取:
JSON.parse(value)反序列化; - 推荐使用
get(key, { type: 'json' }),自动反序列化,简化代码。
KV 的一致性模型:最终一致性:
KV 采用最终一致性,即多区域写入的键值对,同步到全球所有节点需要一定时间(毫秒到秒级)。
- 适用场景:缓存、配置、非强一致的高频数据;
- 不适用场景:需要强一致性的场景(如金融交易、实时状态),此类场景请使用 Durable Objects。
避免频繁的批量操作:
KV 的批量操作(putMany/deleteMany)虽高效,但单次最大支持 1000 个键,且频繁的大批量操作会消耗写配额,建议:
- 批量操作按 1000 个键为一组拆分;
- 非必要不进行全量的批量更新/删除。
注意免费版配额限制:
免费版 KV 有1000 万次读/100 万次写的月配额,超出后会按次计费,建议:
- 对高频读的接口,增加 Cloudflare 全局缓存,减少 KV 读取次数;
- 避免循环调用
get/put,尽量使用批量操作; - 为缓存数据设置合理的 TTL,减少无效的写操作。
元数据的大小限制:
自定义元数据最大为16KB,且不计入 KV 存储容量,适合存储键的附加信息,无需将附加信息写入值中,减少值的大小。
KV 最佳实践与生态整合
KV 核心适用场景
KV 核心适用场景:
KV 是 Cloudflare 边缘开发的基础存储,适合以下所有高频、小体积、非关联型数据场景:
- 接口缓存:缓存高频访问的 API 数据,提升响应速度;
- 系统配置:存储应用的静态配置(如标题、版本、开关),无需修改代码即可更新;
- 会话管理:存储用户的轻量会话信息(如 token、登录状态),替代传统的 Cookie/Session;
- 元数据缓存:存储 R2 文件的元数据(URL、大小、类型)、D1 查询结果的缓存,提升查询性能;
- 临时数据存储:存储验证码、临时令牌等,设置 TTL 自动过期,无需手动清理;
- 特征标记:存储功能开关、AB 测试配置,实现灰度发布。
Cloudflare 生态整合最佳实践
Cloudflare 生态整合最佳实践:
KV 与 Cloudflare 其他服务深度集成,可实现边缘应用的完整数据闭环,推荐组合:
- KV + Hono/Workers:实现边缘缓存、配置管理、轻量数据存储,打造低延迟边缘应用;
- KV + D1:KV 缓存 D1 的查询结果,减少 D1 访问次数,提升接口响应速度;
- KV + R2:KV 存储 R2 文件的元数据(URL、大小、分类),替代 R2 的
list方法,提升查询性能; - KV + Durable Objects:KV 存储高频读的静态数据,Durable Objects 存储强一致的实时状态,互补使用;
- KV + Pages:为 Cloudflare Pages 静态站点提供动态配置能力,无需重新部署即可更新站点内容。